package com.ppch.zerocodegenerator.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import com.ppch.zerocodegenerator.ai.enums.CodeGenTypeEnum;
import com.ppch.zerocodegenerator.core.AiCodeGeneratorFacade;
import com.ppch.zerocodegenerator.core.builder.VueProjectBuilderProd;
import com.ppch.zerocodegenerator.core.code_flux_handler.StreamHandlerExecutor;
import com.ppch.zerocodegenerator.exception.BusinessException;
import com.ppch.zerocodegenerator.exception.ErrorCode;
import com.ppch.zerocodegenerator.exception.ThrowUtils;
import com.ppch.zerocodegenerator.mapper.AppMapper;
import com.ppch.zerocodegenerator.model.constant.AppConstant;
import com.ppch.zerocodegenerator.model.dto.app.AppQueryRequest;
import com.ppch.zerocodegenerator.model.entity.App;
import com.ppch.zerocodegenerator.model.entity.ChatHistory;
import com.ppch.zerocodegenerator.model.entity.User;
import com.ppch.zerocodegenerator.model.enums.ChatHistoryMessageTypeEnum;
import com.ppch.zerocodegenerator.model.vo.AppVO;
import com.ppch.zerocodegenerator.model.vo.UserVO;
import com.ppch.zerocodegenerator.service.AppService;
import com.ppch.zerocodegenerator.service.ChatHistoryService;
import com.ppch.zerocodegenerator.service.ScreenshotService;
import com.ppch.zerocodegenerator.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.io.*;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static com.ppch.zerocodegenerator.ai.model.ExampleApplicationsPrompt.EXAMPLE_APPLICATIONS_PROMPT_PROCESSED_MAP_LOCAL;
import static com.ppch.zerocodegenerator.ai.model.ExampleApplicationsPrompt.EXAMPLE_APPLICATIONS_PROMPT_PROCESSED_MAP_PROD;

/**
 * 应用 服务层实现。
 *
 * @author 泡泡茶壶
 * @since 2025-10-5
 */
@Service
@Slf4j
public class AppServiceImpl extends ServiceImpl<AppMapper, App> implements AppService {

    @Resource
    private UserService userService;

    @Resource
    private AiCodeGeneratorFacade aiCodeGeneratorFacade;

    @Resource
    private ChatHistoryService chatHistoryService;

    @Resource
    private StreamHandlerExecutor streamHandlerExecutor;

    @Resource
    private ScreenshotService screenshotService;

    /**
     * 根据应用ID关联查询创建该应用的用户信息
     *
     * @param app 应用信息
     * @return AppVO（包含创建该应用的用户信息）
     */
    @Override
    public AppVO getAppVO(App app) {
        if (app == null) {
            return null;
        }
        AppVO appVO = new AppVO();
        BeanUtil.copyProperties(app, appVO);
        // 关联查询用户信息
        Long userId = app.getUserId();
        if (userId != null) {
            User user = userService.getById(userId);
            UserVO userVO = userService.getUserVO(user);
            appVO.setUser(userVO);
        }
        return appVO;
    }

    /**
     * 根据请求参数构建查询条件对象 QueryWrapper
     *
     * @param appQueryRequest 请求参数
     * @return QueryWrapper
     */
    @Override
    public QueryWrapper getQueryWrapper(AppQueryRequest appQueryRequest) {
        if (appQueryRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
        }
        Long id = appQueryRequest.getId();
        String appName = appQueryRequest.getAppName();
        String cover = appQueryRequest.getCover();
        String initPrompt = appQueryRequest.getInitPrompt();
        String codeGenType = appQueryRequest.getCodeGenType();
        String deployKey = appQueryRequest.getDeployKey();
        Integer priority = appQueryRequest.getPriority();
        Long userId = appQueryRequest.getUserId();
        String sortField = appQueryRequest.getSortField();
        String sortOrder = appQueryRequest.getSortOrder();
        return QueryWrapper.create()
                .eq(App::getId, id)
                .like(App::getAppName, appName)
                .like(App::getCover, cover)
                .like(App::getInitPrompt, initPrompt)
                .eq(App::getCodeGenType, codeGenType)
                .eq(App::getDeployKey, deployKey)
                .eq(App::getPriority, priority)
                .eq(App::getUserId, userId)
                .orderBy(sortField, "ascend".equals(sortOrder));
    }


    /**
     * 根据应用列表批量获取应用信息，并关联查询创建该应用的用户信息
     *
     * @param appList 应用列表
     * @return 应用信息列表（包含创建该应用的用户信息）
     */
    @Override
    public List<AppVO> getAppVOList(List<App> appList) {
        if (CollUtil.isEmpty(appList)) {
            return new ArrayList<>();
        }
        // 批量获取所有用户信息，避免 N+1 查询问题
        Set<Long> userIds = appList.stream()
                .map(App::getUserId)
                .collect(Collectors.toSet());
        //根据用户ID获取对应的用户信息
        Map<Long, UserVO> userVOMap = userService.listByIds(userIds).stream()
                .collect(Collectors.toMap(User::getId, userService::getUserVO));
        //将应用信息与用户信息关联
        return appList.stream().map(app -> {
            AppVO appVO = this.getAppVO(app);
            UserVO userVO = userVOMap.get(app.getUserId());
            appVO.setUser(userVO);
            return appVO;
        }).collect(Collectors.toList());
    }


    /**
     * 根据用户Prompt以及应用ID调用AI生成/修改代码。
     *
     * @param appId     应用ID
     * @param message   用户Prompt
     * @param loginUser 当前登录用户
     * @return Flux<String> 代码生成结果流
     */
    @Override
    public Flux<String> chatToGenCode(Long appId, String message, User loginUser) {
        // 1. 参数校验
        ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 不能为空");
        ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "用户消息不能为空");
        // 2. 查询应用信息
        App app = this.getById(appId);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR, "应用不存在");
        // 3. 验证用户是否有权限访问该应用，仅本人可以生成代码
        if (!app.getUserId().equals(loginUser.getId())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限访问该应用");
        }
        // 4. 获取应用的代码生成类型
        String codeGenTypeStr = app.getCodeGenType();
        CodeGenTypeEnum codeGenTypeEnum = CodeGenTypeEnum.getEnumByValue(codeGenTypeStr);
        if (codeGenTypeEnum == null) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的代码生成类型");
        }
        // 5. 通过校验后，添加用户消息到对话历史
        chatHistoryService.addChatMessage(appId, message, ChatHistoryMessageTypeEnum.USER.getValue(), loginUser.getId());
        // 6. 调用 AI 生成代码（流式）
        Flux<String> codeStream = aiCodeGeneratorFacade.generateAndSaveCodeWithStream(message, codeGenTypeEnum, appId);
        // 7. 收集AI响应内容并在完成后记录到对话历史
        return streamHandlerExecutor.doExecute(codeStream, chatHistoryService, appId, loginUser, codeGenTypeEnum);
    }


    /**
     * 应用部署
     *
     * @param appId     应用ID
     * @param loginUser 当前登录用户
     * @return 访问路径：CODE_DEPLOY_HOST = "http://localhost"+deployKey;因为本地nginx配置了反向代理，
     * 所以nginx会将请求转发到文件根目录：System.getProperty("user.dir") + "/temp/code_deploy";
     */
    @Override
    public String deployApp(Long appId, User loginUser) {
        // 1. 参数校验
        ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 不能为空");
        ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
        // 2. 查询应用信息
        App app = this.getById(appId);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR, "应用不存在");
        // 3. 验证用户是否有权限部署该应用，仅本人可以部署
        if (!app.getUserId().equals(loginUser.getId())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限部署该应用");
        }
        // 4. 检查是否已有 deployKey（支持多次部署）
        String deployKey = app.getDeployKey();
        // 没有则生成 6 位 deployKey（大小写字母 + 数字）
        if (StrUtil.isBlank(deployKey)) {
            deployKey = RandomUtil.randomString(6);
        }
        // 5. 获取代码生成类型，构建源目录路径 -> 代码生成模式_应用ID
        String codeGenType = app.getCodeGenType();
        String sourceDirName = codeGenType + "_" + appId;
        String sourceDirPath = AppConstant.CODE_OUTPUT_ROOT_DIR + File.separator + sourceDirName;
        // 6. 检查源目录是否存在
        File sourceDir = new File(sourceDirPath);
        if (!sourceDir.exists() || !sourceDir.isDirectory()) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用代码不存在，请先生成代码");
        }
        // 7. Vue 项目特殊处理：需要执行构建（安装依赖，打包项目）
        CodeGenTypeEnum codeGenTypeEnum = CodeGenTypeEnum.getEnumByValue(codeGenType);
        if (codeGenTypeEnum == CodeGenTypeEnum.VUE_PROJECT) {
            // Vue 项目需要构建，执行同步构建
            boolean buildSuccess = new VueProjectBuilderProd().buildProject(sourceDirPath);
            ThrowUtils.throwIf(!buildSuccess, ErrorCode.SYSTEM_ERROR, "Vue 项目构建失败，请检查代码和依赖");
            // 检查 dist 目录是否存在
            File distDir = new File(sourceDirPath, "dist");
            ThrowUtils.throwIf(!distDir.exists(), ErrorCode.SYSTEM_ERROR, "Vue 项目构建完成但未生成 dist 目录");
            // 将 dist 目录作为部署源
            sourceDir = distDir;
            log.info("Vue 项目构建成功，将部署 dist 目录: {}", distDir.getAbsolutePath());
        }
        // 8. 复制文件到部署目录
        String deployDirPath = AppConstant.CODE_DEPLOY_ROOT_DIR + File.separator + deployKey;
        try {
            FileUtil.copyContent(sourceDir, new File(deployDirPath), true);
        } catch (Exception e) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "部署失败：" + e.getMessage());
        }
        // 8. 更新应用的 deployKey 和部署时间
        App updateApp = new App();
        updateApp.setId(appId);
        updateApp.setDeployKey(deployKey);
        updateApp.setDeployedTime(LocalDateTime.now());
        boolean updateResult = this.updateById(updateApp);
        ThrowUtils.throwIf(!updateResult, ErrorCode.OPERATION_ERROR, "更新应用部署信息失败");
        // 9. 构建应用访问 URL
        // TODO：注意，部署项目时需要将域名修改为服务器域名
        String appDeployUrl = String.format("%s/%s/", AppConstant.CODE_DEPLOY_HOST_REMOTE, deployKey);
        // 10. 项目部署完成后，异步生成截图并更新应用封面
        generateAppScreenshotAsync(appId, appDeployUrl);
        // 11. 返回应用访问 URL
        return appDeployUrl;

    }


    /**
     * 异步生成应用截图并更新封面
     *
     * @param appId  应用ID
     * @param appUrl 应用访问URL
     */
    @Override
    public void generateAppScreenshotAsync(Long appId, String appUrl) {
        // 使用虚拟线程异步执行
        Thread.startVirtualThread(() -> {
            // 调用截图服务生成截图并上传
            String screenshotUrl = screenshotService.generateAndUploadScreenshot(appUrl);
            // 更新应用封面字段
            App updateApp = new App();
            updateApp.setId(appId);
            updateApp.setCover(screenshotUrl);
            boolean updated = this.updateById(updateApp);
            ThrowUtils.throwIf(!updated, ErrorCode.OPERATION_ERROR, "更新应用封面字段失败");
        });
    }

    /**
     * 重写 removeById 方法，删除应用时关联删除对话历史
     * 删除应用时关联删除对话历史
     *
     * @param id 应用ID
     * @return 是否成功
     */
    @Override
    public boolean removeById(Serializable id) {
        if (id == null) {
            return false;
        }
        // 转换为 Long 类型
        Long appId = Long.valueOf(id.toString());
        if (appId <= 0) {
            return false;
        }
        // 先删除关联的对话历史
        try {
            chatHistoryService.deleteAllChatHistoryByAppId(appId);
        } catch (Exception e) {
            // 记录日志但不阻止应用删除
            log.error("删除应用关联对话历史失败: {}", e.getMessage());
        }
        // 删除应用
        return super.removeById(id);
    }

    /**
     * 判断用户输入Prompt与示例应用Prompt是否完全一致，如果一致则返回示例应用的appId，否则返回 0
     * @param userPrompt 用户输入Prompt
     * @return 一致：示例应用的appId，否则返回 0
     */
    @Override
    public Long isWhetherConsistentPrompt(String userPrompt) {
        if (StrUtil.isBlank(userPrompt)) {
            return 0L;
        }
        // 对文本进行归一化处理：移除非字母数字汉字字符，合并多个空格
        String normalizeUserPrompt = normalizeText(userPrompt);
        log.warn("原始用户Prompt：{} 归一化后Prompt：{}", userPrompt, normalizeUserPrompt);
        // 如果一致则返回示例应用的appId，否则返回 0
        // TODO：注意，上线前记得修改示例应用 Prompt
        Long exampleAppId = EXAMPLE_APPLICATIONS_PROMPT_PROCESSED_MAP_PROD.entrySet()
                .stream()
                .filter(entry -> normalizeUserPrompt.equals(entry.getValue()))
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(0L);
        log.warn("返回的示例应用appId：{}",exampleAppId);
        return exampleAppId;
    }

    /**
     * 根据示例应用的appId、新应用的appId、当前登录用户，复制示例应用的代码文件、对话历史，
     * 并将示例应用的AI回复返回给前端，无需进入 AI 代码生成。
     * @param exampleApplicationAppId 示例应用的appId
     * @param newAppId 新应用的appId
     * @param loginUser 当前登录用户
     * @return AI 回复（示例应用的 AI 回复）
     */
    @Override
    public String copyCodeFileAndHistory(Long exampleApplicationAppId,Long newAppId,User loginUser) {
        //1.根据示例应用的appId获取对应的应用信息
        App exampleApp = this.getById(exampleApplicationAppId);
        //2.根据应用类型、exampleApplicationAppId构建示例应用代码目录，用于找到示例应用的代码文件
        String codeGenType = exampleApp.getCodeGenType();
        //System.getProperty("user.dir") + /temp/code_output/codeGenType_appId
        String exampleAppSourceDirPath = AppConstant.CODE_OUTPUT_ROOT_DIR + File.separator + codeGenType + "_" + exampleApplicationAppId;
        //检查代码目录是否存在
        File sourceDir = new File(exampleAppSourceDirPath);
        ThrowUtils.throwIf(!sourceDir.exists() || !sourceDir.isDirectory(),
                ErrorCode.NOT_FOUND_ERROR, "示例应用代码不存在，请先生成代码");
        //3.根据应用类型、新应用的appId创建一个目录，并将示例应用的代码文件复制到其中
        String newAppSourceDirPath = AppConstant.CODE_OUTPUT_ROOT_DIR + File.separator + codeGenType + "_" + newAppId;
        //复制代码文件
        try {
            copyDirectory(exampleAppSourceDirPath, newAppSourceDirPath);
            log.info("成功复制代码文件从 {} 到 {}", exampleAppSourceDirPath, newAppSourceDirPath);
        } catch (IOException e) {
            log.error("复制代码文件失败", e);
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "复制代码文件失败");
        }
        //4.根据exampleApplicationAppId、消息类型获取示例应用的对话历史
        QueryWrapper chatHistoryQueryWrapper = new QueryWrapper();
        chatHistoryQueryWrapper.eq(ChatHistory::getAppId,exampleApplicationAppId);
        chatHistoryQueryWrapper.orderBy(ChatHistory::getCreateTime,true);
        List<ChatHistory> exampleApplicationChatHistoryList = chatHistoryService.list(chatHistoryQueryWrapper);
        //5.复制对话历史（user消息和ai消息各一条）
        String aiMessage = "";
        for (ChatHistory exampleApplicationChatHistory : exampleApplicationChatHistoryList) {
            ChatHistory newChathistory = new ChatHistory();
            String message = exampleApplicationChatHistory.getMessage();
            String messageType = exampleApplicationChatHistory.getMessageType();
            newChathistory.setMessage(message);
            newChathistory.setMessageType(messageType);
            newChathistory.setAppId(newAppId);
            newChathistory.setUserId(loginUser.getId());
            if(ChatHistoryMessageTypeEnum.AI.getValue().equals(messageType)){
                aiMessage = message;
                //AI消息要比用户消息的创建、修改时间更晚
                newChathistory.setCreateTime(LocalDateTime.now().plus(1, ChronoUnit.SECONDS));
                newChathistory.setUpdateTime(LocalDateTime.now().plus(1, ChronoUnit.SECONDS));
            }
            boolean result = chatHistoryService.save(newChathistory);
            ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR,"复制示例应用对话历史失败!");
            log.warn("复制示例应用对话历史成功，消息类型：{}！",messageType);
        }
        //6.如果是VUE工程项目还需要对项目进行构建（安装依赖、打包）
        CodeGenTypeEnum codeGenTypeEnum = CodeGenTypeEnum.getEnumByValue(codeGenType);
        if (codeGenTypeEnum == CodeGenTypeEnum.VUE_PROJECT) {
            // Vue 项目需要构建，执行同步构建
            boolean buildSuccess = new VueProjectBuilderProd().buildProject(newAppSourceDirPath);
            ThrowUtils.throwIf(!buildSuccess, ErrorCode.SYSTEM_ERROR, "复制示例应用并对 Vue 项目进行构建失败！");
        }
        //7.返回AI消息历史给前端展示
        return aiMessage;
    }

    /**
     * 对文本进行归一化处理：移除非字母数字汉字字符，合并多个空格
     * @param text 文本
     * @return 处理后的文本
     */
    private String normalizeText(String text) {
        if (text == null) return "";
        return text.toLowerCase()
                .replaceAll("[^\\w\\u4e00-\\u9fa5\\s]", " ") // 移除非字母数字汉字字符
                .replaceAll("\\s+", " ") // 合并多个空格
                .trim();
    }


    /**
     * 复制目录及其所有子文件和子目录，但排除 dist 和 node_modules 目录
     * @param sourceDirPath 源目录路径
     * @param targetDirPath 目标目录路径
     * @throws IOException 复制过程中可能抛出的IO异常
     */
    private void copyDirectory(String sourceDirPath, String targetDirPath) throws IOException {
        File sourceDir = new File(sourceDirPath);
        File targetDir = new File(targetDirPath);

        // 如果目标目录不存在，则创建
        if (!targetDir.exists()) {
            boolean created = targetDir.mkdirs();
            if (!created) {
                throw new IOException("无法创建目标目录: " + targetDirPath);
            }
        }

        // 获取源目录下的所有文件和子目录
        File[] files = sourceDir.listFiles();
        if (files == null) {
            throw new IOException("无法读取源目录: " + sourceDirPath);
        }

        for (File file : files) {
            // 跳过 dist 和 node_modules 目录
            if (file.isDirectory() &&
                    ("dist".equals(file.getName()) || "node_modules".equals(file.getName()))) {
                continue; // 跳过这些目录及其所有内容
            }

            File targetFile = new File(targetDir, file.getName());

            if (file.isDirectory()) {
                // 递归复制子目录
                copyDirectory(file.getAbsolutePath(), targetFile.getAbsolutePath());
            } else {
                // 复制文件
                copyFile(file, targetFile);
            }
        }
    }

    /**
     * 复制单个文件
     * @param sourceFile 源文件
     * @param targetFile 目标文件
     * @throws IOException 复制过程中可能抛出的IO异常
     */
    private void copyFile(File sourceFile, File targetFile) throws IOException {
        try (InputStream in = new FileInputStream(sourceFile);
             OutputStream out = new FileOutputStream(targetFile)) {

            byte[] buffer = new byte[1024];
            int length;
            while ((length = in.read(buffer)) > 0) {
                out.write(buffer, 0, length);
            }
        }
    }

}
